Skip to content

fix: await network server bind completion - #760

Open
yanhu7150-tech wants to merge 1 commit into
jetlinks:2.11from
yanhu7150-tech:bugfix-network-server-bind-lifecycle
Open

fix: await network server bind completion#760
yanhu7150-tech wants to merge 1 commit into
jetlinks:2.11from
yanhu7150-tech:bugfix-network-server-bind-lifecycle

Conversation

@yanhu7150-tech

Copy link
Copy Markdown

问题背景

TCP、HTTP 和 MQTT 网络服务均基于 Vert.x 的异步 listen/绑定接口启动。
修复前,三个网络服务提供器在发起异步监听后,没有等待底层 Vert.x Server 的绑定结果,就提前向上游返回了已经创建的 Network 实例。这会导致网络服务的响应式生命周期与实际端口监听状态不一致。
具体表现包括:

  1. 当目标端口已被占用时,提供器返回的 Mono 仍可能先成功完成,而实际的端口绑定异常在之后才异步发生。
  2. 调用方收到 Network 实例时,底层 TCP、HTTP 或 MQTT 服务可能尚未真正开始监听。
  3. 配置多个监听实例时,如果部分实例绑定成功、后续实例绑定失败,已经启动的实例可能无法被完整回收。
  4. 启动过程中发生异常时,清理操作没有被纳入完整的异步错误传播流程。
  5. 在启动阶段取消订阅时,已经完成绑定的服务可能继续存活。
  6. isAlive() 主要依赖底层 Server 的端口状态,无法准确区分“底层端口已经产生”与“提供器整体启动已经成功完成”。
    这些问题会导致调用方错误判断网络服务已经可用,并使端口冲突、部分启动失败和取消启动等场景下的状态与资源管理不可靠。

根因分析

问题的根因是网络提供器没有将 Vert.x 异步监听结果组合到其返回的响应式调用链中。
原有流程大致为:

  1. 创建多个 Vert.x Server;
  2. 调用异步监听接口;
  3. 不等待全部监听操作完成;
  4. 立即返回 Network 实例。
    因此,提供器返回的 Mono 完成,只能表示“监听操作已经发起”,不能表示“全部监听操作已经成功”。
    同时,Server 实现中缺少明确的“整体启动完成”状态。仅根据 Server 集合或 actualPort 判断存活状态,无法准确表示提供器级别的启动生命周期。

修复方案

本次修改统一调整了 TCP、HTTP 和 MQTT 网络服务的启动、失败清理、取消及关闭流程。

1. 等待全部监听操作完成后再返回 Network

三个网络提供器现在会:

  1. 创建并安装完整的 Vert.x Server 集合;
  2. 发起所有 Server 的异步监听操作;
  3. 将所有监听结果纳入同一个响应式调用链;
  4. 等待全部监听操作成功完成;
  5. 标记网络服务整体启动成功;
  6. 最后才向调用方返回 Network 实例。
    因此,提供器返回的 Mono 成功完成时,可以确定对应的网络服务已经完成实际端口绑定。

2. 正确传播端口绑定异常

当任意一个监听操作失败时,例如:

  • 端口已被占用;
  • 地址绑定失败;
  • Vert.x 监听操作异常;
    异常会通过提供器返回的 Mono 传递给调用方,不再出现“提供器已经成功返回,但端口绑定随后失败”的情况。

3. 清理部分启动的 Server

当多个 Server 中的部分实例已经成功绑定、后续实例启动失败时,提供器会触发统一的异步关闭流程,清理已经创建或已经开始监听的所有 Server。
清理完成后,继续向上游传播原始的绑定或监听异常,避免清理操作覆盖真正的启动失败原因。

4. 处理启动阶段的取消

监听调用链增加了取消处理。
当订阅者在启动过程中取消订阅时,会关闭当前创建的网络服务,避免已经绑定的端口继续占用系统资源。

5. 增加明确的启动完成状态

TCP、HTTP 和 MQTT Server 实现均增加了显式启动状态,用于区分:

  • Server 对象已经创建;
  • 底层端口可能已经产生;
  • 提供器整体启动已经成功完成。
    只有全部监听操作成功之后,才会将该状态设置为已启动。
    重新安装 Server 集合、清理 Server 或关闭网络服务时,启动状态会同步重置。

6. 收紧 isAlive() 判定条件

修改后的 isAlive() 需要同时满足:

  • 提供器整体启动已经完成;
  • 已安装的 Server 集合不为空;
  • Server 集合中存在有效实例;
  • 底层 Server 处于有效监听状态。
    这样可以防止底层 actualPort 已经为正数、但提供器整体启动尚未完成时,错误地将网络服务报告为存活。

7. 统一同步和异步关闭流程

Server 实现新增了用于异常清理的异步关闭能力,并统一处理:

  • 启动失败后的清理;
  • 主动关闭;
  • 启动阶段取消;
  • Server 集合清空;
  • 启动状态重置。
    关闭过程中会先提取并清空当前 Server 集合,避免同一批 Server 被重复关闭或继续被 isAlive() 使用。

涉及范围

本次修改仅涉及以下 6 个网络服务生产代码文件:

TCP

  • DefaultTcpServerProvider.java
  • VertxTcpServer.java

HTTP

  • DefaultHttpServerProvider.java
  • VertxHttpServer.java

MQTT

  • DefaultVertxMqttServerProvider.java
  • VertxMqttServer.java
    本次修改未涉及:
  • 公共 API 变更;
  • Maven 依赖或 POM 配置变更;
  • GitHub Actions 工作流变更;
  • 网络核心接口变更;
  • 其他无关组件变更。
    现有 HTTP Server 构造方法签名保持不变,避免影响已有调用方的二进制兼容性。

修复后的预期行为

修复后,网络服务具有以下行为:

  1. 提供器成功返回 Network 时,端口已经真正完成绑定。
  2. 端口冲突会直接使提供器返回的 Mono 失败。
  3. 多 Server 启动过程中任意实例失败,已经启动的实例会被统一关闭。
  4. 启动失败后不会残留被占用的监听端口。
  5. 启动阶段取消订阅不会留下仍在运行的 Server。
  6. isAlive() 只会在提供器整体启动完成后返回 true
  7. 调用 shutdown() 后,启动状态会重置,监听端口会被释放。

验证情况

针对 TCP、HTTP 和 MQTT 三种协议,分别验证了以下场景:

  1. 端口占用场景
    预先占用目标端口后启动网络服务,确认提供器返回的 Mono 以绑定异常结束,而不是提前返回成功结果。
  2. 正常监听场景
    提供器成功返回后,使用真实连接确认 TCP、HTTP 或 MQTT Server 已经开始监听。
  3. 关闭与端口释放场景
    调用关闭方法后,确认:
    • isAlive() 返回 false
    • 原监听端口被释放;
    • 端口可以被重新绑定。
  4. 启动完成状态场景
    在底层 Server 已经产生有效 actualPort、但尚未调用整体启动完成逻辑时,确认 isAlive() 仍然返回 false
  5. 取消订阅场景
    在网络实例返回附近立即取消订阅,确认不会留下仍然存活、仍然可以连接的监听服务。
    以上定向场景已分别对 TCP、HTTP 和 MQTT 执行,并进行了重复验证。

构建验证

Windows 完整编译

执行:

.\mvnw.cmd -DskipTests clean compile
结果:
43/43 modules SUCCESS
BUILD SUCCESS
Ubuntu / GitHub Pull Request CI 等价验证
使用 Java 17,并执行 2.11 Pull Request 工作流中的同一条命令:
./mvnw package -Dmaven.test.skip=true -Pbuild
结果:
43/43 reactor modules SUCCESS
Maven exit code: 0
BUILD SUCCESS
构建完成后,Git 工作区保持干净,当前提交未发生变化。

@CLAassistant

CLAassistant commented Jul 23, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@yanhu7150-tech

Copy link
Copy Markdown
Author

@superzoc @karlyli @wujun8 您们好,Codacy 静态代码分析当前未通过,但根据分析日志,该问题并非由本 PR 修改的 Java 代码引起。
本 PR 仅修改了 TCP、HTTP 和 MQTT 网络服务生命周期相关的 6 个 Java 生产代码文件。Codacy 中与 Java 相关的以下检查均已正常完成:

  • PMD
  • Java Metrics
  • Java Duplication
    Codacy 的 Issues 页面也未显示本 PR 新增的代码问题。
    当前失败发生在已经标记为 deprecated 的 ESLint 分析阶段,错误信息如下:
Error on file jetlinks-manager/device-manager/src/main/resources/typescript/transparent-codec.d.ts:1.
Cause: Parsing error: Cannot read file '/src/tsconfig.json'.
Error on file jetlinks-components/network-component/tcp-component/src/main/resources/typescript/ScriptPayloadParser.d.ts:1.
Cause: Parsing error: Cannot read file '/src/tsconfig.json'.
Error on file jetlinks-components/network-component/tcp-component/src/main/resources/typescript/vertx.d.ts:1.
Cause: Parsing error: Cannot read file '/src/tsconfig.json'.

上述 3 个 TypeScript 声明文件均未被本 PR 修改。本 PR 的实际变更范围仅包括以下文件:

jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/DefaultHttpServerProvider.java
jetlinks-components/network-component/http-component/src/main/java/org/jetlinks/community/network/http/server/vertx/VertxHttpServer.java
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/server/vertx/DefaultVertxMqttServerProvider.java
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/server/vertx/VertxMqttServer.java
jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/server/DefaultTcpServerProvider.java
jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/server/VertxTcpServer.java

从日志来看,失败原因是 Codacy 的 ESLint 分析环境无法读取 /src/tsconfig.json。后续的 DiffDeltas 步骤也未能正常完成,应当是前述分析异常导致的连锁失败。
由于我是外部贡献者,没有该 Codacy 项目的重新分析权限,因此无法点击 retry your analysis
麻烦维护者在方便时帮助重新运行 Codacy 检查;若重新运行后仍然出现相同的 /src/tsconfig.json 错误,也请协助确认该仓库级 ESLint 配置异常是否可以忽略或绕过。
本 PR 已完成以下验证:

  • TCP 网络服务生命周期定向测试通过;
  • HTTP 网络服务生命周期定向测试通过;
  • MQTT 网络服务生命周期定向测试通过;
  • 端口占用时的绑定异常能够通过返回的 Mono 正确传播;
  • 正常启动完成后端口已实际进入监听状态;
  • 关闭后 isAlive() 返回 false,且监听端口能够被释放;
  • 启动未整体完成时,即使底层 actualPort 已产生,也不会错误报告为存活;
  • 启动或返回阶段取消订阅后,不会遗留仍在监听的 Server;
  • Windows 全仓编译通过;
  • Ubuntu Java 17 环境下的官方 Pull Request 工作流等价构建通过。
    Ubuntu 验证使用了官方 2.11 Pull Request 工作流中的同一条命令:
./mvnw package -Dmaven.test.skip=true -Pbuild

验证结果:

43/43 reactor modules SUCCESS
Maven exit code: 0
BUILD SUCCESS

谢谢您们啦~

@zhou-hao

Copy link
Copy Markdown
Member

设计如此, 如果 启动失败立即反馈错误, 可能在服务重启等静默启动时, 导致依赖相关组件的地方无法引用. 后续重新修改网络组件可能无法热生效. 将当前运行状态,健康度,原因等缓存到网络组件内部,然后提供接口获取当前状态展示到前端应该更合适.

@yanhu7150-tech

Copy link
Copy Markdown
Author

设计如此, 如果 启动失败立即反馈错误, 可能在服务重启等静默启动时, 导致依赖相关组件的地方无法引用. 后续重新修改网络组件可能无法热生效. 将当前运行状态,健康度,原因等缓存到网络组件内部,然后提供接口获取当前状态展示到前端应该更合适.

明白了,感谢您指出问题。
我理解的更合适的处理方式是:

  1. 网络组件对象仍然正常创建并进入管理流程,不因一次监听失败而丢失;
  2. 在网络组件内部维护明确的运行状态,例如 STARTINGRUNNINGFAILEDSTOPPED
  3. 底层监听成功后,将状态更新为运行中;
  4. 监听失败时清理已经部分启动的 Server,但不使整个网络组件创建流程失败;
  5. 缓存最近一次启动失败的异常、原因、发生时间及相关运行信息;
  6. 后续修改配置时,仍然能够基于现有网络组件重新启动并热生效;
  7. 通过接口暴露当前运行状态、健康度和失败原因,供管理端或前端展示。
    您看是这样吗?如果是的话,我准备先检查当前网络组件已有的状态抽象、健康检查接口、配置热更新流程以及管理端使用的相关接口,再基于现有设计调整实现,这样就可以避免重新增加一套不一致的状态模型。
    不过我还想向您确认两个实现范围的问题:
  8. 当前运行状态、健康度和失败原因,是否希望优先复用或扩展现有的网络组件接口呢?
  9. 本次 PR 是否需要同时包含状态查询接口和前端展示,还是先完成网络组件内部状态维护以及后端接口?
    麻烦您有空时回复一下我,谢谢您啦~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants